Add iOS .stringsdict ↔ gettext PO conversion actions#739
Draft
jkmassel wants to merge 1 commit into
Draft
Conversation
Collaborator
Generated by 🚫 Danger |
jkmassel
added a commit
that referenced
this pull request
Jun 21, 2026
jkmassel
added a commit
that referenced
this pull request
Jun 22, 2026
Round-trip iOS `.stringsdict` plural files through gettext `.po`/`.pot` so plurals can be translated in a system like GlotPress — the existing `.strings` pipeline is key/value only and can't represent plurals. - `ios_generate_pot_from_stringsdict` (forward): English `.stringsdict` → `.pot`, one `msgid`/`msgid_plural` entry per plural variable (`msgctxt` is the key, or `key:variable` for multi-variable entries). - `ios_generate_stringsdict_from_po` (reverse): translated `.po` + the English `.stringsdict` as a structural template → per-locale `.stringsdict`, mapping indexed `msgstr[N]` back to CLDR category names and back-filling the iOS-required `other`. Returns the contexts left untranslated (kept as English). `Ios::PluralRules` maps a locale to its ordered CLDR categories. Because the pipeline consumes GlotPress `.po` exports and GlotPress lags CLDR for several locales, the table is generated (`rakelib/generate_ios_plural_rules.rb`) from GlotPress `GP_Locales` (form count) × CLDR (category names) rather than from CLDR alone; the reverse converter reads each `.po`'s `Plural-Forms` header and fails loud if the count drifts from the table. Locales whose GlotPress rule can't map to CLDR (e.g. Welsh) fail with a clean user error. New helpers `Ios::StringsdictHelper` and `Ios::PluralRules`; no new runtime dependencies (`plist`/`gettext` were already in the gemspec). [#739]
b55e3e4 to
fe9b425
Compare
Round-trip iOS `.stringsdict` plural files through gettext `.po`/`.pot` so plurals can be translated in a system like GlotPress — the existing `.strings` pipeline is key/value only and can't represent plurals. - `ios_generate_pot_from_stringsdict` (forward): English `.stringsdict` → `.pot`, one `msgid`/`msgid_plural` entry per plural variable (`msgctxt` is the key, or `key:variable` for multi-variable entries). - `ios_generate_stringsdict_from_po` (reverse): translated `.po` + the English `.stringsdict` as a structural template → per-locale `.stringsdict`, mapping indexed `msgstr[N]` back to CLDR category names and back-filling the iOS-required `other`. Returns the contexts left untranslated (kept as English). `Ios::PluralRules` maps a locale to its ordered CLDR categories. Because the pipeline consumes GlotPress `.po` exports and GlotPress lags CLDR for several locales, the table is generated (`rakelib/generate_ios_plural_rules.rb`) from GlotPress `GP_Locales` (form count) × CLDR (category names) rather than from CLDR alone; the reverse converter reads each `.po`'s `Plural-Forms` header and fails loud if the count drifts from the table. Locales whose GlotPress rule can't map to CLDR (e.g. Welsh) fail with a clean user error. New helpers `Ios::StringsdictHelper` and `Ios::PluralRules`; no new runtime dependencies (`plist`/`gettext` were already in the gemspec). [#739]
fc2b121 to
ee18adb
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Adds first-class support in Release Toolkit for round-tripping iOS .stringsdict plural resources through gettext .pot/.po, enabling plural translations via GlotPress while preserving .stringsdict structure via an English template and locale-specific plural category mapping.
Changes:
- Introduces
ios_generate_pot_from_stringsdictandios_generate_stringsdict_from_poFastlane actions for forward/reverse conversion between.stringsdictand gettext. - Adds iOS helper modules for
.stringsdictplist I/O + conversion logic and a generated locale→CLDR-category mapping table aligned to GlotPress plural-form counts. - Adds fixtures, generator inputs, and RSpec coverage for conversions, plural-category mapping, and failure modes (unmapped locales, drift in plural form counts, partial translations).
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| spec/test-data/translations/stringsdict/simple.stringsdict | Fixture for a single-variable .stringsdict plural entry. |
| spec/test-data/translations/stringsdict/Localizable.stringsdict | Fixture covering multi-entry and multi-variable .stringsdict cases. |
| spec/ios_stringsdict_helper_spec.rb | Unit tests for .stringsdict read/write + .pot generation + reverse conversion from .po. |
| spec/ios_plural_rules_spec.rb | Unit tests validating locale normalization, category mappings, and incompatible locales. |
| spec/ios_generate_stringsdict_from_po_spec.rb | Action-level tests for generating localized .stringsdict files from .po + template. |
| spec/ios_generate_pot_from_stringsdict_spec.rb | Action-level tests for generating .pot templates from .stringsdict. |
| rakelib/plural_rules_data/glotpress_nplurals.json | Vendored GlotPress plural-form counts (input to rule-table generator). |
| rakelib/plural_rules_data/cldr_plurals.xml | Vendored CLDR plural categories data (input to rule-table generator). |
| rakelib/generate_ios_plural_rules.rb | Generator script combining GlotPress nplurals + CLDR category names into the runtime table. |
| lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_stringsdict_helper.rb | Core conversion logic between .stringsdict and gettext .po/.pot. |
| lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_plural_rules.rb | Generated locale→ordered plural-category mapping aligned to GlotPress outputs. |
| lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_generate_stringsdict_from_po.rb | Fastlane action wrapper for reverse conversion (.po → localized .stringsdict). |
| lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_generate_pot_from_stringsdict.rb | Fastlane action wrapper for forward conversion (English .stringsdict → .pot). |
| CHANGELOG.md | Documents the new iOS plural round-trip actions and behavior/limitations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+205
to
+207
| def self.context_for(key:, variable:, single_variable:) | ||
| single_variable ? key : "#{key}:#{variable}" | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Round-trips iOS
.stringsdictplural files through gettext.po/.potso plurals can be translated in GlotPress — today's.stringspipeline is key/value only and can't represent plurals.What it adds
ios_generate_pot_from_stringsdict(forward): English.stringsdict→.pot; each plural variable becomes onemsgid/msgid_pluralentry, keyed bymsgctxt= the stringsdict key (key:variablefor multi-variable entries).ios_generate_stringsdict_from_po(reverse): translated.po+ the English.stringsdictas a structural template (the.pocarries only strings) → per-locale.stringsdict. Maps indexedmsgstr[N]to CLDR category names, back-fills the iOS-requiredother, and returns the contexts left untranslated (filled from the English source).Ios::StringsdictHelper(plist I/O + conversions) andIos::PluralRules(locale → ordered categories, generated).CLDR support — why it isn't "full"
iOS plural selection is CLDR-based, so "fully correct" would mean full CLDR. We deliberately match GlotPress instead, because it produces the
.poand lags CLDR for several locales (Romance stays 2-form; Hebrew 2 vs 3; Welsh on a legacy 4-form rule) with no fix imminent — two abandoned PRs (GlotPress/GlotPress#692,#784) under the still-open umbrella issue#666, and ~27% of locales skipped in GlotPress's own CLDR test. SoIos::PluralRulesis generated from GlotPressGP_Locales(form count) × CLDR (category names); being "more CLDR than GlotPress" would expect forms the.ponever contains. Regenerate viarakelib/generate_ios_plural_rules.rb(vendored inputs).Across all 29 WordPress-iOS locales, 27 produce correct plurals (Indonesian harmlessly over-supplies an inert form). The two exceptions:
cy) — its legacy 4-form rule can't map to CLDR's six categories, so the converter raises a clean error; excludecyfrom the locales you convert (in-app, Welsh then uses its non-plural.stringsentry).he) — 2-form vs CLDR's three, so the dual isn't supplied andn = 2renders with the plural form. Degraded, not wrong.Both self-heal: the reverse converter reads
npluralsfrom each.po'sPlural-Formsheader and fails loud if it stops matching the table — so when GlotPress fixes a locale, the pipeline tells us to regenerate.Test plan
bundle exec rspec— 39 examples across the 4 new specs, 0 failuresbundle exec rubocop— no offensesrakelib/generate_ios_plural_rules.rbreproduces the committed table exactlyNotes
No new runtime dependencies —
plist/gettextwere already in the gemspec; vendored CLDR/GlotPress data lives underrakelib/(dev-only, not shipped). The diff is over Danger's 500-line soft limit, but it's one cohesive feature.